Skip to main content

JavaScript Promises and Event Loop

Event Loop and Task Queues​

JavaScript's concurrency model is based on the "event loop", which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks.

Microtasks vs Macrotasks​

  • Microtasks: These are tasks that are processed after the currently executing script and before the event loop continues. Examples include promises and queueMicrotask.
  • Macrotasks: These include tasks like setTimeout, setInterval, and I/O tasks.

Microtasks vs Macrotasks Order of Execution​

Promise.resolve().then(() => console.log(1));

queueMicrotask(() => console.log(2));

setTimeout(() => console.log(3), 0);

console.log(4);

new Promise(() => console.log(5));

(async () => console.log(6))();

Expected Output:

4
5
6
1
2
3

Explanation:​

  1. Synchronous tasks run first, so console.log(4) and console.log(5) are executed.
  2. Async/Await is essentially syntactic sugar for promises, so console.log(6) is executed next.
  3. Promises (.then()) and microtasks (queueMicrotask) are processed in the order they appear.
  4. Macrotasks like setTimeout are executed last, even if they have a delay of 0.

Complex Promises and Async/Await Flow​

In more complex scenarios involving promises and async/await, understanding the order of execution becomes even more critical.

Async Function with Multiple Promises​

async function asyncFunction() {
console.log(1);

new Promise(() => console.log(2));

await new Promise((resolve) =>
setTimeout(() => {
console.log(3);
resolve();
}, 0),
);

console.log(4);
}

new Promise(() => {
console.log(5);

(async () => {
console.log(6);
await asyncFunction();
console.log(7);
})();

console.log(8);
}).then(() => console.log(9));

Expected Output:

5
6
8
1
2
3
4
7
9

Explanation:​

  1. console.log(5), console.log(6), and console.log(8) are executed synchronously within the first promise.
  2. The async function is called, starting with console.log(1) and console.log(2).
  3. The await inside asyncFunction delays the next synchronous log (console.log(4)) until after the setTimeout callback executes (console.log(3)).
  4. Once all microtasks are finished, the final .then() is executed, printing console.log(9).

Understanding the Execution Order​

Combining Promises, Async Functions, and Tasks​

(async () => {
const asyncFunc = async () => "asyncFunc";

const promise = new Promise((resolve) => {
console.log("promise");
resolve();
}).then(() => console.log("then"));

console.log("async body");

queueMicrotask(() => console.log("queueMicrotask"));

const results = await Promise.all([asyncFunc(), promise]);

return results;
})();

console.log("script");

Expected Output:

promise
async body
script
queueMicrotask
then

Explanation:​

  1. console.log('promise') and console.log('async body') are logged synchronously.
  2. The console.log('script') runs after the synchronous code block.
  3. The queueMicrotask runs after all synchronous code but before the .then() callback.
  4. The console.log('then') is executed last as part of the promise resolution.

Key Takeaways​

  • Promises are part of the microtask queue, which runs before macrotasks.
  • Async/Await pauses function execution until the awaited promise resolves, but microtasks and synchronous code can still execute.
  • Understanding the JavaScript event loop is crucial for debugging and writing asynchronous code effectively.